// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.eclipse.che.ide.ui.tree;

import elemental.css.CSSStyleDeclaration;
import elemental.dom.Element;
import elemental.html.DivElement;
import elemental.html.SpanElement;
import elemental.js.html.JsLIElement;
import elemental.js.html.JsUListElement;
import org.eclipse.che.ide.util.AnimationController;
import org.eclipse.che.ide.util.CssUtils;
import org.eclipse.che.ide.util.dom.DomUtils;
import org.eclipse.che.ide.util.dom.Elements;

/**
 * Overlay type for the base element for a Node in the tree.
 *
 * <p>Nodes with no children have no UL element.
 *
 * <p>Nodes that have children, but that have never been expanded (nodes render lazily on
 * expansion), have an empty UL element.
 *
 * <p>
 *
 * <pre>
 *
 * <li class="treeNode">
 *   <div class="treeNodeBody">
 *     <div class="expandControl"></div><span class="treeNodeLabel"></span>
 *   </div>
 *   <ul class="childrenContainer">
 *   </ul>
 * </li>
 *
 * </pre>
 */
public class TreeNodeElement<D> extends JsLIElement {

  /**
   * Creates a TreeNodeElement from some data. Should only be called by {@link Tree}.
   *
   * @param <D> the type of data
   * @param dataAdapter An {@link NodeDataAdapter} that allows us to visit the NodeData
   * @return a new {@link TreeNodeElement} created from the supplied data.
   */
  static <D> TreeNodeElement<D> create(
      D data,
      NodeDataAdapter<D> dataAdapter,
      NodeRenderer<D> nodeRenderer,
      Tree.Css css,
      Tree.Resources resources) {

    @SuppressWarnings("unchecked")
    TreeNodeElement<D> treeNode = (TreeNodeElement<D>) Elements.createElement("li", css.treeNode());
    treeNode.setData(data);
    treeNode.setRenderer(nodeRenderer);

    // Associate the rendered node with the underlying model data.
    dataAdapter.setRenderedTreeNode(data, treeNode);

    // Attach the Tree node body.
    DivElement nodeBody = Elements.createDivElement(css.treeNodeBody());
    nodeBody.setAttribute("draggable", "true");
    treeNode.appendChild(nodeBody);

    // Attach expand node element
    DivElement expand = Elements.createDivElement();
    Elements.addClassName(css.expandControl(), expand);
    nodeBody.appendChild(expand);

    expand.setInnerHTML(
        resources.collapsedIcon().getSvg().getElement().getString()
            + resources.expandedIcon().getSvg().getElement().getString());
    ((Element) expand.getChildNodes().item(1)).getStyle().setDisplay("none");

    SpanElement nodeContent = nodeRenderer.renderNodeContents(data);
    Elements.addClassName(css.treeNodeLabel(), nodeContent);
    nodeBody.appendChild(nodeContent);

    // Attach the Tree node children.
    treeNode.ensureChildrenContainer(dataAdapter, css);

    return treeNode;
  }

  protected TreeNodeElement() {}

  public final void updateLeafOffset(Element parent) {
    if (!parent.hasAttribute("___depth")) {
      return;
    }

    try {
      int depth = Integer.parseInt(parent.getAttribute("___depth"));

      Element expandElement = (Element) getNodeBody().getChildren().item(0);
      expandElement.getStyle().setMarginLeft("" + (depth * 8) + "px");

      if (!hasChildNodes()) {
        return;
      }
      final JsUListElement childrenContainer = getChildrenContainer();
      if (childrenContainer != null) {
        getChildrenContainer().setAttribute("___depth", "" + (depth + 1));
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Appends the specified child to this TreeNodeElement's child container element.
   *
   * @param child The {@link TreeNodeElement} we want to append to as a child of this node.
   */
  public final void addChild(
      NodeDataAdapter<D> dataAdapter, TreeNodeElement<D> child, Tree.Css css) {
    ensureChildrenContainer(dataAdapter, css);
    getChildrenContainer().appendChild(child);
  }

  /** @return The associated NodeData that is a bound to this node when it was rendered. */
  public final native D getData() /*-{
        return this.__nodeData;
    }-*/;

  /**
   * Nodes with no children have no UL element, only a DIV for the Node body.
   *
   * @return whether or not this node has children.
   */
  public final boolean hasChildrenContainer() {
    int length = this.getChildren().getLength();

    assert (length < 3) : "TreeNodeElement has more than 2 children of its root element!";

    return (length == 2);
  }

  public final boolean isActive(Tree.Css css) {
    return CssUtils.containsClassName(getSelectionElement(), css.active());
  }

  /** Checks whether or not this node is open. */
  public final native boolean isOpen() /*-{
        return !!this.__nodeOpen;
    }-*/;

  private void setOpen(Tree.Css css, boolean isOpen) {
    if (isOpen != isOpen()) {
      if (isOpen) {
        ((Element) getExpandControl().getChildNodes().item(0)).getStyle().setDisplay("none");
        ((Element) getExpandControl().getChildNodes().item(1)).getStyle().setDisplay("block");
      } else {
        ((Element) getExpandControl().getChildNodes().item(0)).getStyle().setDisplay("block");
        ((Element) getExpandControl().getChildNodes().item(1)).getStyle().setDisplay("none");
      }

      setOpenImpl(isOpen);
      getRenderer().updateNodeContents(this);
    }
  }

  private native void setOpenImpl(boolean isOpen) /*-{
        this.__nodeOpen = isOpen;
    }-*/;

  public final boolean isSelected(Tree.Css css) {
    return CssUtils.containsClassName(getSelectionElement(), css.selected());
  }

  /** Makes this node into a leaf node. */
  public final void makeLeafNode(Tree.Css css) {
    getExpandControl().setClassName(css.expandControl() + " " + css.leafIcon());
    if (hasChildrenContainer()) {
      DomUtils.removeFromParent(getChildrenContainer());
    }
  }

  /**
   * Removes this node from the {@link Tree} and breaks the back reference from the underlying node
   * data.
   */
  public final void removeFromTree() {
    DomUtils.removeFromParent(this);
  }

  //    /** Sets whether or not this node has the active node styling applied. */
  //    public final void setActive(boolean isActive, Tree.Css css) {
  //        // Show the selection on the element returned by the node renderer
  //        Element selectionElement = getSelectionElement();
  //        CssUtils.setClassNameEnabled(selectionElement, css.active(), isActive);
  //        if (isActive) {
  //            selectionElement.focus();
  //        }
  //    }

  /** Sets whether or not this node has the selected styling applied. */
  public final void setSelected(boolean selected, boolean active, Tree.Css css) {
    Elements.removeClassName(css.selected(), getSelectionElement());
    Elements.removeClassName(css.selectedInactive(), getSelectionElement());

    if (selected) {
      Elements.addClassName(
          active ? css.selected() : css.selectedInactive(), getSelectionElement());
      getSelectionElement().setAttribute("selected", "true");
    } else {
      getSelectionElement().removeAttribute("selected");
    }
  }

  /** Sets whether or not this node is the active drop target. */
  public final void setIsDropTarget(boolean isDropTarget, Tree.Css css) {
    CssUtils.setClassNameEnabled(this, css.isDropTarget(), isDropTarget);
  }

  /**
   * Closes the current node. Must have children if you call this!
   *
   * @param css The {@link Tree.Css} instance that contains relevant selector names.
   * @param shouldAnimate whether to do the animation or not
   */
  final void closeNode(
      NodeDataAdapter<D> dataAdapter,
      Tree.Css css,
      AnimationController closer,
      boolean shouldAnimate) {
    ensureChildrenContainer(dataAdapter, css);

    Element expandControl = getExpandControl();

    assert (hasChildrenContainer()
            && CssUtils.containsClassName(expandControl, css.expandControl()))
        : "Tried to close a node that didn't have an expand control";

    setOpen(css, false);

    Element childrenContainer = getChildrenContainer();
    if (shouldAnimate) {
      closer.hide(childrenContainer);
    } else {
      closer.hideWithoutAnimating(childrenContainer);
    }
  }

  /**
   * You should call hasChildren() before calling this method. This will throw an exception if a
   * Node is a leaf node.
   *
   * @return The UL element containing children of this TreeNodeElement.
   */
  final JsUListElement getChildrenContainer() {
    return (JsUListElement) this.getChildren().item(1);
  }

  public final SpanElement getNodeLabel() {
    return (SpanElement) getNodeBody().getChildren().item(1);
  }

  /**
   * Expands the current node. Must have children if you call this!
   *
   * @param css The {@link Tree.Css} instance that contains relevant selector names.
   * @param shouldAnimate whether to do the animation or not
   */
  final void openNode(
      NodeDataAdapter<D> dataAdapter,
      Tree.Css css,
      AnimationController opener,
      boolean shouldAnimate) {
    ensureChildrenContainer(dataAdapter, css);

    Element expandControl = getExpandControl();

    assert (hasChildrenContainer()
            && CssUtils.containsClassName(expandControl, css.expandControl()))
        : "Tried to open a node that didn't have an expand control";

    setOpen(css, true);

    Element childrenContainer = getChildrenContainer();
    if (shouldAnimate) {
      opener.show(childrenContainer);
    } else {
      opener.showWithoutAnimating(childrenContainer);
    }
  }

  /**
   * If this node does not have a children container, but has children data, then we coerce a
   * children container into existence.
   */
  final void ensureChildrenContainer(NodeDataAdapter<D> dataAdapter, Tree.Css css) {
    if (!hasChildrenContainer()) {
      D data = getData();
      if (dataAdapter.hasChildren(data)) {
        Element childrenContainer = Elements.createElement("ul", css.childrenContainer());
        this.appendChild(childrenContainer);
        childrenContainer.getStyle().setDisplay(CSSStyleDeclaration.Display.NONE);
        ((Element) getExpandControl().getChildNodes().item(0)).getStyle().setDisplay("block");
        ((Element) getExpandControl().getChildNodes().item(1)).getStyle().setDisplay("none");

      } else {
        getExpandControl().setClassName(css.expandControl() + " " + css.leafIcon());
      }
    }
  }

  private Element getExpandControl() {
    return (Element) getNodeBody().getChildren().item(0);
  }

  /** @return The node body element that contains the expansion control and the node contents. */
  private Element getNodeBody() {
    return (Element) getChildren().item(0);
  }

  final Element getSelectionElement() {
    return getNodeBody();
  }

  /**
   * Stashes associate NodeData as an expand on our element, and also sets up a reverse mapping.
   *
   * @param data The NodeData we want to associate with this node element.
   */
  private native void setData(D data) /*-{
        this.__nodeData = data;
    }-*/;

  private native NodeRenderer<D> getRenderer() /*-{
        return this.__nodeRenderer;
    }-*/;

  private native void setRenderer(NodeRenderer<D> renderer) /*-{
        this.__nodeRenderer = renderer;
    }-*/;
}
